package cc.chinagps.gateway.client;

import cc.chinagps.gateway.client.bean.*;
import cc.chinagps.gateway.client.codec.ResponseMessageDecoder;
import cc.chinagps.gateway.client.handler.*;
import cc.chinagps.gateway.client.packet.BasePacket;
import cc.chinagps.gateway.client.packet.RequestPacket;
import cc.chinagps.gateway.client.packet.ResponsePacket;
import cc.chinagps.gateway.client.packet.down.*;
import cc.chinagps.gateway.client.redis.RedisManager;
import cc.chinagps.gateway.client.session.ISession;
import cc.chinagps.gateway.client.session.UnitClientSession;
import cc.chinagps.gateway.common.Config;
import cc.chinagps.gateway.log.LogManager;
import cc.chinagps.gateway.unit.eg.upload.bean.EGRealTimeInfo;
import cc.chinagps.gateway.util.*;
import com.seggps.protobuf.LastPosDataBuff;
import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufUtil;
import io.netty.channel.*;
import io.netty.channel.epoll.Epoll;
import io.netty.channel.epoll.EpollEventLoopGroup;
import io.netty.channel.epoll.EpollSocketChannel;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.timeout.IdleStateHandler;
import lombok.Data;
import lombok.ToString;

import java.util.concurrent.TimeUnit;

/**
 * 待处理
 * 1、断线重连，包括主动断开、被动断开、读写超时、服务器端异常等
 * 2、补传数据，服务器断开，缓存终端数据，连上之后，发送数据
 * 3、断连重试策略选择
 * 4、多协议支持
 */
@Data
public class UnitClient {
    @ToString.Exclude
    private Bootstrap bootstrap;
    @ToString.Exclude
    private EventLoopGroup group;
    private String host = Config.SYS.TRANSFER_SERVER_IP;
    private Integer port;
    @ToString.Exclude
    private Integer gpsPort = Config.SYS.TRANSFER_SERVER_PORT;
    @ToString.Exclude
    private Integer obdPort = Config.SYS.TRANSFER_SERVER_OBD_PORT;
    private ISession session;
    private ProtocolType protocolType;
    private DeviceInfo deviceInfo;
    private Integer retries = 0;
    private Long lastLoginTime = -1L;
    private Long lastGpsTime = -1L;
    private Long lastDieselTime = -1L;
    private Long lastObdTime = -1L;
    private boolean reLogin = false;
    private Long gpsDeliverInterval = Config.SYS.GPS_DELIVER_INTERVAL;
    private Long dieselDeliverInterval = Config.SYS.DIESEL_DELIVER_INTERVAL;
    private Long loginDeliverInterval = Config.SYS.LOGIN_DELIVER_INTERVAL;
    private Long obdDeliverInterval = Config.SYS.OBD_DELIVER_INTERVAL;
    @ToString.Exclude
    private RetryPolicy retryPolicy;
    @ToString.Exclude
    private String lastPosPrefix = "LASTPOS:";
    private String lastObdHashKey = "transferLastObd";
    @ToString.Exclude
    private byte[] invalidIntBytes = new byte[]{(byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF};
    private static final long READER_IDLE_TIME = Config.SYS.DEVICE_IDLE_TIME_MAX;
    private static final long WRITER_IDLE_TIME = Config.SYS.DEVICE_IDLE_TIME_MAX;
    private static final long ALL_IDLE_TIME = Config.SYS.DEVICE_IDLE_TIME_MAX;
    private boolean checkDeliverIntervalEnabled = Config.SYS.CHECK_DELIVER_INTERVAL_ENABLED;
    private Gps lastGps;
    private ObdBaseInfo lastObdBaseInfo;
    private RequestPacket curRequestPacket;
    private ResponsePacket curResponsePacket;

    public UnitClient(DeviceInfo deviceInfo) {
        if (deviceInfo.getTerminalType() == TerminalType.OBD.getType()) {
            this.port = obdPort;
        } else {
            this.port = gpsPort;
        }
        this.deviceInfo = deviceInfo;
        init();
    }

    private void init() {
        bootstrap = new Bootstrap();
        boolean supportEpoll = Epoll.isAvailable();
        if (supportEpoll) {
            group = new EpollEventLoopGroup();
            bootstrap.channel(EpollSocketChannel.class);
        } else {
            group = new NioEventLoopGroup();
            bootstrap.channel(NioSocketChannel.class);
        }
        this.bootstrap.group(group)
                .handler(supportEpoll ? new ClientEpollSocketInitialzer(this) : new ClientNioSocketInitialzer(this));
    }

    public void connect() {
        if (session != null && session.isConnected()) {
            return;
        }
        ChannelFuture future = bootstrap.connect(host, port);
        future.addListener(getConnectionListener());
    }

    private ChannelFutureListener getConnectionListener() {
        return (future) -> {
            if (!future.isSuccess()) {
                LogManager.t("[{}] unitClient connect to the server failed,ip:{},port:{}", deviceInfo.getTerminalNo(), host, port);
                final EventLoop eventLoop = future.channel().eventLoop();
                reconnect(eventLoop, 8L, TimeUnit.SECONDS, false);
            } else {
                LogManager.t("[{}] unitClient connect to the server success,ip:{},port:{}", deviceInfo.getTerminalNo(), host, port);
            }
        };
    }

    public void reconnect(ChannelHandlerContext ctx) {
        boolean closing = session.isClosing();
        UnitClientManager.instance.removeClient(this);
        if (!closing && deviceInfo.getTransferEnabled()) {
            boolean allowRetry = getRetryPolicy().allowRetry(retries);
            if (allowRetry) {
                long sleepTimeMs = getRetryPolicy().getSleepTimeMs(retries);
                retries++;
                final EventLoop eventLoop = ctx.channel().eventLoop();
                reconnect(eventLoop, sleepTimeMs, TimeUnit.MILLISECONDS, true);
            }
        }
    }

    /**
     * @param eventLoop    EventLoop
     * @param l            延迟时间
     * @param timeUnit     时间单位
     * @param hasConnected 是否有连接上过
     */
    private void reconnect(final EventLoop eventLoop, long l, TimeUnit timeUnit, boolean hasConnected) {
        if (!Config.SYS.RECONNECT_ENABLED || !deviceInfo.getTransferEnabled()) {
            return;
        }
        eventLoop.schedule(() -> {
            if (hasConnected) {
                LogManager.t("[{}] unitClient Lost connection,try to reconnect the server,ip:{},port:{}", deviceInfo.getTerminalNo(), host, port);
            } else {
                LogManager.t("[{}] unitClient try to reconnect to the server,ip:{},port:{}", deviceInfo.getTerminalNo(), host, port);
            }
            connect();
        }, l, timeUnit);
    }

    public void initSession(ChannelHandlerContext ctx) throws Exception {
        initData();
        session = new UnitClientSession(ctx);
        session.setRemoteSocketAddress(ctx.channel().remoteAddress());
        session.setLocalSocketAddress(ctx.channel().localAddress());
        session.setConnected(true);
        session.setProtocolType(protocolType);
        login();
    }

    /**
     * 一个完整的连接周期内，看是否需要清除数据
     */
    private void initData() {
        //本应重试连接成功之后，将retries清0，但是重连成功之后会发送登录包，服务端会检测登录包，
        //如果不合理，会强制关闭客户端的连接，客户端因为将retries清0，会永无休止的重试，
        //retries的值因为连接成功置为0，永远不会超过设置的重试次数阈值
        //其实登录包或者其他数据包，比如位置数据包，都有可能引起这个问题
        //如果单单是因为服务端连接断开，这个retries就需要置为0

        //retries = 0;
        //lastLoginTime = -1L;
        //lastGpsTime = -1L;
        //reLogin = false;
    }

    private RetryPolicy getRetryPolicy() {
        if (this.retryPolicy == null) {
            this.retryPolicy = new ExponentialBackOffRetry(this, 1000, Config.SYS.MAX_RETRY_COUNT, 60 * 1000);
        }
        return this.retryPolicy;
    }

    public void initChannel(SocketChannel ch) throws Exception {
        //final ClientMessageDecoder messageDecoder = new ClientMessageDecoder(this);
        //final UnitClientHandler clientHandler = new UnitClientHandler(this);
        //ch.pipeline().addLast(new IdleStateHandler(READER_IDLE_TIME, WRITER_IDLE_TIME, ALL_IDLE_TIME, TimeUnit.SECONDS));
        //ch.pipeline().addLast(new UnitClientReconnectHandler(this));
        //ch.pipeline().addLast(new LengthFieldBasedFrameDecoder(Integer.MAX_VALUE, 21, 2, 2, 0, true));
        //ch.pipeline().addLast(messageDecoder);
        //ch.pipeline().addLast(clientHandler);

        final ResponseMessageDecoder responseMessageDecoder = new ResponseMessageDecoder(this);
        final ResponseMessageHandler responseMessageHandler = new ResponseMessageHandler(this);
        ch.pipeline().addLast(new IdleStateHandler(READER_IDLE_TIME, WRITER_IDLE_TIME, ALL_IDLE_TIME, TimeUnit.MILLISECONDS));
        ch.pipeline().addLast(new UnitClientReconnectHandler(this));
        ch.pipeline().addLast(responseMessageDecoder);
        ch.pipeline().addLast(responseMessageHandler);
    }

    public boolean sendData(byte[] data) throws Exception {
        if (session == null) {
            return false;
        }
        boolean res = session.sendData(data);
        LogManager.t("[{}] unitClient send data:{}", deviceInfo.getTerminalNo(), HexUtil.byteToHexStr(data));
        return res;
    }

    public boolean sendData(ByteBuf data) throws Exception {
        if (session == null) {
            return false;
        }
        LogManager.t("[{}] unitClient send data:{}", deviceInfo.getTerminalNo(), ByteBufUtil.hexDump(data));
        return session.sendData(data);
    }

    public boolean sendData(RequestPacket requestPacket) throws Exception {
        if (session == null) {
            return false;
        }
        ByteBuf data = requestPacket.getData();
        LogManager.t("[{}] unitClient send requestPacket:{}", deviceInfo.getTerminalNo(), requestPacket);
        boolean result = session.sendData(data);
        if (result) {
            curRequestPacket = requestPacket;
        }
        return result;
    }

    public void login() throws Exception {
        if (!Config.SYS.TRANSFER_ENABLED) {
            return;
        }
        if (session == null || !session.isConnected()) {
            return;
        }

        if (session.isAuthed()) {
            return;
        }
        if (lastLoginTime != -1 && checkDeliverIntervalEnabled) {
            if (System.currentTimeMillis() - lastLoginTime < loginDeliverInterval) {
                //由于某种原因被服务器强制断开连接，不能立即登录，将需要登录的终端标记，达到指定的间隔之后进行登录，
                //未到间隔之前，session是已经连接未认证状态，不转发数据
                reLogin = true;
                LogManager.t("[{}] do not reach the login deliverInterval,skip this login,lastLoginTime:{}", deviceInfo.getTerminalNo(), DateTimeUtil.getDateTimeAsStringGMT8(lastLoginTime, DateTimeUtil.DATETIME_FORMAT));
                return;
            }
        }
        ByteBuf loginData = createLoginData();
        boolean res = sendData(new RequestPacket(PacketType.LOGIN, loginData));
        if (res) {
            lastLoginTime = System.currentTimeMillis();
            if (Config.SYS.SUCCESS_MODE == 1) {
                session.setAuthed(true);
                reLogin = false;
            }
        }
    }

    public void logout() throws Exception {
        if (session != null && !session.isAuthed()) {
            return;
        }
        ByteBuf logoutData = createLogoutData();
        sendData(new RequestPacket(PacketType.LOGOUT, logoutData));
        session.setClosing(true);
        session.close();
        LogManager.t("[{}] device logout,close the unitClient", deviceInfo.getTerminalNo());
    }

    public void sendGps(Integer longitude, Integer latitude, String gpsTime, Integer status) throws Exception {
        if (session != null && !session.isAuthed()) {
            return;
        }
        if (lastGpsTime != -1 && checkDeliverIntervalEnabled) {
            if (System.currentTimeMillis() - lastGpsTime < gpsDeliverInterval) {
                LogManager.t("[{}] do not reach the gps deliverInterval,skip this gps data,gpsTime:{},lastGpsTime:{}", deviceInfo.getTerminalNo(), gpsTime, DateTimeUtil.getDateTimeAsStringGMT8(lastGpsTime, DateTimeUtil.DATETIME_FORMAT));
                return;
            }
        }
        ByteBuf gps = createGps(longitude, latitude, gpsTime, status);
        boolean res = sendData(new RequestPacket(PacketType.GPS, gps));
        if (res) {
            lastGpsTime = System.currentTimeMillis();
        }
    }

    public void sendDieselRealTimeInfo(EGRealTimeInfo realTimeInfo, DieselRealTimeInfo dieselRealTimeInfo) throws Exception {
        if (session != null && !session.isAuthed()) {
            return;
        }
        if (lastDieselTime != -1 && checkDeliverIntervalEnabled) {
            if (System.currentTimeMillis() - lastDieselTime < dieselDeliverInterval) {
                LogManager.t("[{}] do not reach the DieselRealTimeInfo deliverInterval,skip this DieselRealTimeInfo data,dieselRealTimeInfoTime:{},lastDieselTime:{}", deviceInfo.getTerminalNo(), realTimeInfo.getCollectTimeStr(), DateTimeUtil.getDateTimeAsStringGMT8(lastDieselTime, DateTimeUtil.DATETIME_FORMAT));
                return;
            }
        }

        ByteBuf diesel = createDieselRealTimeInfo(realTimeInfo, dieselRealTimeInfo);
        boolean res = sendData(new RequestPacket(PacketType.DIESEL, diesel));
        if (res) {
            lastDieselTime = System.currentTimeMillis();
        }
    }

    public void sendObdBaseInfo() throws Exception {
        if (session != null && !session.isAuthed()) {
            return;
        }
        String hGet = RedisManager.instance.hget(lastObdHashKey, deviceInfo.getDeviceId());
        if (hGet == null) {
            lastObdTime = -1L;
        } else {
            lastObdTime = Long.parseLong(hGet);
        }
        if (lastObdTime != -1 && checkDeliverIntervalEnabled) {
            if (System.currentTimeMillis() - lastObdTime < obdDeliverInterval) {
                LogManager.t("[{}] do not reach the obd deliverInterval,skip this obd data,lastObdTime:{}", deviceInfo.getTerminalNo(), DateTimeUtil.getDateTimeAsStringGMT8(lastObdTime, DateTimeUtil.DATETIME_FORMAT));
                return;
            }
        }

        ByteBuf obd = createObd();
        if (obd == null) {
            return;
        }
        boolean res = sendData(new RequestPacket(PacketType.OBD, obd));
        if (res) {
            lastObdTime = System.currentTimeMillis();
            RedisManager.instance.hset(lastObdHashKey, deviceInfo.getDeviceId(), lastObdTime + "");
        }
    }

    public void sendTerminalRemovedAlarm(Integer longitude, Integer latitude, String gpsTime, Integer status) throws Exception {
        if (session != null && !session.isAuthed()) {
            return;
        }
        ByteBuf alarm = createTerminalRemovedAlarm(longitude, latitude, gpsTime, status);
        sendData(new RequestPacket(PacketType.TERMINAL_REMOVED_ALARM, alarm));
    }

    public ByteBuf createLoginData() throws Exception {
        BasePacket packet = new BasePacket((byte) 0x01, deviceInfo);
        Packet01 packet01 = new Packet01(packet);
        Integer terminalType = deviceInfo.getTerminalType();
        packet01.setIccid(deviceInfo.getIccid());
        /*if (terminalType == TerminalType.OBD.getType()) {
            packet01.setIccid(deviceInfo.getIccid());
        } else {
            packet01.setIccid(deviceInfo.getTerminalNo());
        }*/
        packet01.setSerialNo(MsgSNManager.getInstance().getSn(session));
        packet01.setLoginTime(DateTimeUtil.getCurrentTimeGTM8(DateTimeUtil.DATETIME_FORMAT_SIMPLE));
        if (lastGps == null) {
            String key = lastPosPrefix + deviceInfo.getDeviceId();
            String field = lastPosPrefix + deviceInfo.getDeviceId();
            LastPosDataBuff.LastPosInfo lastPosInfo = RedisManager.instance.getLastPosInfo(key, field);
            if (lastPosInfo != null) {
                packet01.setLongitude((int) (lastPosInfo.getLon() * 1000000));
                packet01.setLatitude((int) (lastPosInfo.getLat() * 1000000));
            } else {
                packet01.setLongitude(ConvertUtil.getInt(invalidIntBytes));
                packet01.setLatitude(ConvertUtil.getInt(invalidIntBytes));
            }
        } else {
            packet01.setLongitude(lastGps.getLongitude());
            packet01.setLatitude(lastGps.getLatitude());
        }
        ByteBuf encoded = packet01.encode();
        LogManager.t("[{}] createLoginData,BasePacket:{},dataInfo:{}", deviceInfo.getTerminalNo(), packet, packet01);
        return encoded;
    }

    public ByteBuf createLogoutData() throws Exception {
        BasePacket packet = new BasePacket((byte) 0x04, deviceInfo);
        Packet04 packet04 = new Packet04(packet);
        packet04.setSerialNo(MsgSNManager.getInstance().getSn(session));
        packet04.setLogoutTime(DateTimeUtil.getCurrentTimeGTM8(DateTimeUtil.DATETIME_FORMAT_SIMPLE));
        if (lastGps == null) {
            String key = lastPosPrefix + deviceInfo.getDeviceId();
            String field = lastPosPrefix + deviceInfo.getDeviceId();
            LastPosDataBuff.LastPosInfo lastPosInfo = RedisManager.instance.getLastPosInfo(key, field);
            if (lastPosInfo != null) {
                packet04.setLongitude((int) (lastPosInfo.getLon() * 1000000));
                packet04.setLatitude((int) (lastPosInfo.getLat() * 1000000));
            } else {
                packet04.setLongitude(ConvertUtil.getInt(invalidIntBytes));
                packet04.setLatitude(ConvertUtil.getInt(invalidIntBytes));
            }
        } else {
            packet04.setLongitude(lastGps.getLongitude());
            packet04.setLatitude(lastGps.getLatitude());
        }
        ByteBuf encoded = packet04.encode();
        LogManager.t("[{}] createLogoutData,BasePacket:{},dataInfo:{}", deviceInfo.getTerminalNo(), packet, packet04);
        return encoded;
    }

    public ByteBuf createGps(Integer longitude, Integer latitude, String gpsTime, Integer status) throws Exception {
        BasePacket packet = new BasePacket((byte) 0x09, deviceInfo);
        Packet09 packet09 = new Packet09(packet);
        packet09.setSerialNo(MsgSNManager.getInstance().getSn(session));
        packet09.setSendTime(gpsTime);
        if (status != null) {
            packet09.setStatus(status.byteValue());
        }
        packet09.setLatitude(latitude);
        packet09.setLongitude(longitude);
        ByteBuf encoded = packet09.encode();
        LogManager.t("[{}] createGps,BasePacket:{},dataInfo:{}", deviceInfo.getTerminalNo(), packet, packet09);
        return encoded;
    }

    public ByteBuf createObd() throws Exception {
        if (lastObdBaseInfo == null) {
            return null;
        }
        DieselRealTimeInfo dieselRealTimeInfo = new DieselRealTimeInfo();
        dieselRealTimeInfo.setObdBaseInfo(lastObdBaseInfo);
        byte[] sendTime = DateTimeUtil.getCurrentDateTimeGTM8();
        byte[] collectTime = DateTimeUtil.getCurrentDateTimeGTM8();
        byte infoType = (byte) 0x01;
        BasePacket packet = new BasePacket((byte) 0x02, infoType, deviceInfo);
        Packet02 packet02 = new Packet02(packet);
        packet02.setSerialNo(MsgSNManager.getInstance().getSn(session));
        packet02.setSendTime(sendTime);
        packet02.setInfoType(infoType);
        packet02.setCollectTime(collectTime);
        packet02.setRealTimeInfo(dieselRealTimeInfo);
        ByteBuf encoded = packet02.encode();
        LogManager.t("[{}] createObd,BasePacket:{},dataInfo:{}", deviceInfo.getTerminalNo(), packet, packet02);
        return encoded;
    }

    public ByteBuf createDieselRealTimeInfo(EGRealTimeInfo realTimeInfo, DieselRealTimeInfo dieselRealTimeInfo) throws Exception {
        byte[] sendTime = DateTimeUtil.getCurrentDateTimeGTM8();
        byte[] collectTime = DateTimeUtil.str2DateTime(realTimeInfo.getCollectTimeStr().substring(2));
        byte infoType = (byte) 0x02;
        BasePacket packet = new BasePacket((byte) 0x02, infoType, deviceInfo);
        Packet02 packet02 = new Packet02(packet);
        packet02.setSerialNo(MsgSNManager.getInstance().getSn(session));
        packet02.setSendTime(sendTime);
        packet02.setInfoType(infoType);
        packet02.setCollectTime(collectTime);
        packet02.setRealTimeInfo(dieselRealTimeInfo);
        ByteBuf encoded = packet02.encode();
        LogManager.t("[{}] createDieselRealTimeInfo,BasePacket:{},dataInfo:{}", deviceInfo.getTerminalNo(), packet, packet02);
        return encoded;
    }

    public ByteBuf createTerminalRemovedAlarm(Integer longitude, Integer latitude, String alarmTime, Integer status) throws Exception {
        BasePacket packet = new BasePacket((byte) 0x06, deviceInfo);
        Packet06 packet06 = new Packet06(packet);
        packet06.setSerialNo(MsgSNManager.getInstance().getSn(session));
        packet06.setSendTime(alarmTime);
        if (status != null) {
            packet06.setStatus(status.byteValue());
        }
        packet06.setLatitude(latitude);
        packet06.setLongitude(longitude);
        ByteBuf encoded = packet06.encode();
        LogManager.t("[{}] createTerminalRemovedAlarm,BasePacket:{},dataInfo:{}", deviceInfo.getTerminalNo(), packet, packet06);
        return encoded;
    }

}
